package gameEngine;

import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import player.MoveTimeOutException;
import player.Player;
import player.PlayerNotRespondingException;
import rulesControl.ByoYomi;
import rulesControl.MovementApprovers;
import rulesControl.Rules;

import gameBasicElements.Board;
import gameBasicElements.Move;
import gameBasicElements.PlayerColor;
import gameBasicElements.Position;
import gameBasicElements.ScoreBoard;
import gameEngine.SGFGame.WrongSGFFileException;

/**
 * @author Igor Adamski
 *
 */
public class Game {
	private Rules rules;
	private ByoYomi byoYomi;
	private Player player1;
	private Player player2;
	private GameHistory history;
	private GameInfo info;
	private GameMessage lastMessage;
	private ActualGame game;
	public Game(Rules rules, Player player1, Player player2) {
		this.rules = rules;
		this.player1 = player1;
		this.player2 = player2;
		this.history = new GameHistory(this.rules.getBoard());
		this.byoYomi = rules.getByoYomi();
		this.game = new ActualGame();
		this.info = new LocalGameInfo(this.game);
	}
	public void begin() {
		game.start();
	}
	public GameHistory getGameHistory() {
		return history;
	}
	public GameInfo getGameInfo() {
		return info;
	}
	public String getSGFString() {
		return SGFGame.createSGFString(rules, history);
	}
	public static SGFGame loadSGF(String sgf) throws WrongSGFFileException {
		return new SGFGame(sgf);
	}
	private void setGameMessage(GameMessage error) {
		lastMessage = error;
		synchronized(info) {
			info.notifyAll();
		}
	}
	public void stopGame() {
		game.gameFinished = true;
		try {
			game.players.get(PlayerColor.BLACK).nextTurn();
			game.players.get(PlayerColor.WHITE).nextTurn();
		}
		catch(PlayerNotRespondingException e) {}
		game.interrupt();
	}
	public ScoreBoard getScoreBoard() {
		return rules.getScores(game.getCurrentBoard());
	}
	public Rules getRules() {
		return rules;
	}
	public class ActualGame extends Thread {
		private boolean gameStarted;
		private boolean gameFinished;
//		private boolean gameIsOn;
		private GameStatus gameStatus = GameStatus.NOT_STARTED;
		private long moveStartTime;
		private PlayerColor doingMove;
		private PlayerColor currentPlayerColor;
		private Map<PlayerColor, Player> players = new TreeMap<PlayerColor, Player>();
		private ActualGame() {
			players.put(PlayerColor.BLACK, player1);
			players.put(PlayerColor.WHITE, player2);
		}
		private MovementApprovers validMove(Position pos, PlayerColor playerColor) {
			Move move = new Move(pos, playerColor);
			for(MovementApprovers i : rules.getApprovers())
				if(!i.approve(history, move))
					return i;
			return null;
		}
		private Position getPlayerMove(final PlayerColor color) throws MoveTimeOutException {
			ExecutorService e = Executors.newCachedThreadPool();
			long startTime = new Date().getTime();
			moveStartTime = startTime;
			doingMove = color;
			Future<Position> ret = e.submit(new Callable<Position>() {
				@Override
				public Position call() throws Exception {
					return players.get(color).getMove();
				}
			});
			e.shutdown();
			try {
				if(!e.awaitTermination(byoYomi.getTimeLeft(color), TimeUnit.MILLISECONDS)) {
					e.shutdownNow();
					throw new MoveTimeOutException();
				}
			}
			catch(InterruptedException e2) {}
			int timeUsed = (int)(new Date().getTime() - startTime);
			byoYomi.update(color, timeUsed);
			doingMove = null;
			
			try {
				return ret.get();
			}
			catch(Exception e1) {}
			return null;
		}
		private void nextTurn(Board board, Move move) {
			history.nextTurn(board, move);
			GameMessage.description("");
			setGameMessage(GameMessage.OK);
			currentPlayerColor = currentPlayerColor.next();
			try {
				player1.nextTurn();
			}
			catch (PlayerNotRespondingException e) {
				gameStatus = GameStatus.CONNECTION_LOST;
				GameMessage.description("czarny gracz nie odpowiada");
				setGameMessage(GameMessage.NETWORK_CONNECTION_PROBLEM);
				stopGame();
			}
			try {
				player2.nextTurn();
			}
			catch (PlayerNotRespondingException e) {
				gameStatus = GameStatus.CONNECTION_LOST;
				GameMessage.description("biały gracz nie odpowiada");
				setGameMessage(GameMessage.NETWORK_CONNECTION_PROBLEM);
				stopGame();
			}
		}
		/**
		 * @param playerColor
		 * @return false if player passed, true otherwise
		 */
		private boolean makeMove(PlayerColor playerColor) {
			MovementApprovers badMove = null;
			Position pos = null;
			try {
				while((badMove = validMove(pos = getPlayerMove(playerColor), playerColor)) != null)
				{
					GameMessage.description(badMove.toString());
					setGameMessage(GameMessage.WRONG_MOVE);
				}
			}
			catch(MoveTimeOutException e) {
				gameStatus = GameStatus.TIMED_OUT;
				GameMessage.description("");
				setGameMessage(GameMessage.TIME_LIMIT_EXCEEDED);
				stopGame();
			}
			Move move = new Move(pos, playerColor);
			nextTurn(info.getCurrentBoard().update(move), move);
			return pos != null;
		}
		private void initGame() {
			gameStarted = true;
//			gameIsOn = true;
			try {
				player1.startGame(info, PlayerColor.BLACK);
			}
			catch (PlayerNotRespondingException e) {
				synchronized(info)
				{
					gameStatus = GameStatus.CANNOT_ESTABILISH_CONNECTION;
					GameMessage.description("gracz czarny nie odpowiada");
					setGameMessage(GameMessage.NETWORK_CONNECTION_PROBLEM);
					info.notifyAll();
					stopGame();
				}
			}
			try {
				player2.startGame(info, PlayerColor.WHITE);
			}
			catch (PlayerNotRespondingException e) {
				synchronized(info)
				{
					gameStatus = GameStatus.CANNOT_ESTABILISH_CONNECTION;
					System.out.println(e);
					GameMessage.description("gracz biały nie odpowiada");
					setGameMessage(GameMessage.NETWORK_CONNECTION_PROBLEM);
					info.notifyAll();
					stopGame();
				}
			}
			currentPlayerColor = PlayerColor.BLACK;
			try {
				player1.nextTurn();
			}
			catch (PlayerNotRespondingException e) {
				synchronized (info)
				{
					gameStatus = GameStatus.CONNECTION_LOST;
					GameMessage.description("gracz czarny nie odpowiada");
					setGameMessage(GameMessage.NETWORK_CONNECTION_PROBLEM);
					info.notifyAll();
					stopGame();
				}
			}
			try {
				player2.nextTurn();
			}
			catch(PlayerNotRespondingException e) {
				synchronized (info)
				{
					gameStatus = GameStatus.CONNECTION_LOST;
					GameMessage.description("gracz biały nie odpowiada");
					setGameMessage(GameMessage.NETWORK_CONNECTION_PROBLEM);
					info.notifyAll();
					stopGame();
				}
			}
			synchronized (info) {
				gameStatus = GameStatus.OK;
				GameMessage.description("");
				setGameMessage(GameMessage.GAME_STARTED);
				info.notifyAll();
			}
			for(int foo=0;foo<rules.getHandicap().getHandicapStones();++foo)
			{
				makeMove(PlayerColor.BLACK);
			}
			if(rules.getHandicap().getHandicapStones() != 0)
				currentPlayerColor = PlayerColor.WHITE;
		}

		private void startGame() {
			boolean lastPassed = false;
			while(true)
			{
				boolean didMove = makeMove(currentPlayerColor);
				if(lastPassed && !didMove)
					break;
				lastPassed = !didMove;
			}
		}
		
		private void endGame() {
			synchronized(info)
			{
				gameStatus = GameStatus.SUCCESSFULLY_ENDED;
				gameFinished = true;
				info.notifyAll();
			}
			ScoreBoard finalScores = rules.getScores(getCurrentBoard());
			try {
				player1.endGame(finalScores);
			}
			catch (PlayerNotRespondingException e) {
				synchronized(info)
				{
					gameStatus = GameStatus.CONNECTION_LOST;
					info.notifyAll();
					stopGame();
				}
			}
			try {
				player2.endGame(finalScores);
			}
			catch (PlayerNotRespondingException e) {
				synchronized(info)
				{
					gameStatus = GameStatus.CONNECTION_LOST;
					info.notifyAll();
					stopGame();
				}
			}
			synchronized (info) {
				info.notifyAll();
			}
		}
		
		@Override
		public void run() {
			initGame();
			startGame();
			endGame();
		}
		
		public Board getCurrentBoard() {
			return history.getBoardAtTurn(history.getCurrentTurn());
		}
		public PlayerColor getCurrentPlayer() {
			return currentPlayerColor;
		}
		public int getTimeLeft(PlayerColor color) {
			int ret = byoYomi.getTimeLeft(color);
			if(doingMove == color)
				ret -= (new Date().getTime() - moveStartTime);
			ret = Math.max(ret, 0);
			return ret;
		}
		public String getLastMoveError() {
			return lastMessage == null ? "" : lastMessage.toString();
		}
		public boolean gameStarted() {
			return gameStarted;
		}
		public boolean gameFinished() {
			return gameFinished;
		}
		public GameStatus getGameStatus() {
			return gameStatus;
		}
		public ScoreBoard getFinalScores() {
			return rules.getScores(getCurrentBoard());
		}
	}
}
